package tx.core.input;

import java.awt.AWTException;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.util.ArrayList;
import java.util.List;

import javax.swing.SwingUtilities;

import tx.development.Development;

/**
 * Manages the input of key and mouse events. 
 * Events are mapped to GameActions.
 * 
 * This class is based on InputManager, by David Brackeen.
 * @author Guilherme Mauro Germoglio Barbosa - germoglio@gmail.com
 *
 */
public class InputManager implements KeyListener, MouseListener, MouseMotionListener, MouseWheelListener {

	/**
	 * A invisible cursor.
	 */
	public static final Cursor INVISIBLE_CURSOR = Toolkit.getDefaultToolkit().createCustomCursor(
			Toolkit.getDefaultToolkit().getImage(""), new Point(0,0), "invisible");
	
	/**
	 * Mouse code for moving left.
	 */
	public static final int MOUSE_MOVE_LEFT = 0;
	
	/**
	 * Mouse code for moving right.
	 */
	public static final int MOUSE_MOVE_RIGHT = 1;
	
	/**
	 * Mouse code for moving up.
	 */
	public static final int MOUSE_MOVE_UP = 2;
	
	/**
	 * Mouse code for moving down.
	 */
	public static final int MOUSE_MOVE_DOWN = 3;
	
	/**
	 * Mouse code for wheel moving up. 
	 */
	public static final int MOUSE_WHEEL_UP = 4;
	
	/**
	 * Mouse code for wheel moving down.
	 */
	public static final int MOUSE_WHEEL_DOWN = 5;
	
	/**
	 * Mouse code for button 1.
	 */
	public static final int MOUSE_BUTTON_1 = 6;
	
	/**
	 * Mouse code for button 2.
	 */
	public static final int MOUSE_BUTTON_2 = 7;
	
	/**
	 * Mouse code for button 3.
	 */
	public static final int MOUSE_BUTTON_3 = 8;
	
	/**
	 * Number of existing mouse codes.
	 */
	private static final int NUM_MOUSE_CODES = 9;
	
	/**
	 * Number of key codes.
	 */
	private static final int NUM_KEY_CODES = 600;
	
	/**
	 * The key game actions.
	 */
	private GameAction[] keyActions = new GameAction[NUM_KEY_CODES];
	
	/**
	 * The mouse game actions.
	 */
	private GameAction[] mouseActions = new GameAction[NUM_MOUSE_CODES];
	
	/**
	 * The current mouse location. 
	 */
	private Point mouseLocation;
	
	/**
	 * The center of screen location.
	 */
	private Point centerLocation;
	
	/**
	 * Component used from which will be listening to input actions.
	 */
	private Component comp;
	
	/**
	 * The robot used to modify window.
	 */
	private Robot robot;
	
	/**
	 * Whether is recentering or not. 
	 */
	private boolean isRecentering;
	
	/**
	 * Creates a new InputManager that listens to input from the specified component.
	 * @param comp The specified component.
	 */
	public InputManager(Component comp) {
		this.comp = comp;
		mouseLocation = new Point();
		centerLocation = new Point();
		
		comp.addKeyListener(this);
		comp.addMouseListener(this);
		comp.addMouseMotionListener(this);
		comp.addMouseWheelListener(this);
		
		comp.setFocusTraversalKeysEnabled(false);
	}
	
	/**
	 * Sets the cursor on this InputManager's input component.
	 * @param cursor The cursor.
	 */
	public void setCursor(Cursor cursor) {
		comp.setCursor(cursor);
	}
	
	/**
	 * Sets whether relative mouse mode is on or not. For relative mouse mode, the mouse is "locked" in the center
	 * of the screen, and only the changed in mouse movement is measured. In normal mode, the mouse is free to
	 * move about the screen.
	 * @param mode <code>true</code> if is to be in relative mouse mode, <code>false</code> otherwise.
	 */
	public void setRelativeMouseMode(boolean mode) {
		if (mode == isRelativeMouseMode()) {
			return;
		}
		
		if (mode) {
			try {
				robot = new Robot();
				recenterMouse();
			} catch (AWTException e) {
				if (Development.DEBUG_ENABLED) 
					System.out.println(Development.DEBUG_LABEL + "InputManager.setRelativeMouseMode() " + e);
				robot = null;
			}
		} else {
			robot = null;
		}
	}
	
	/**
	 * Returns whether or not relative mouse mode is on.
	 * @return <code>true</code> if relative mouse mode is on, <code>false</code> otherwise.
	 */
	public boolean isRelativeMouseMode() {
		return robot != null;
	}
	
	/**
	 * Maps a GameAction to a specific key. The key codes are defined in java.awt.KeyEvent. If the key
	 * already has a GameAction mapped to it, the new GameAction overwrites it.
	 * @param gameAction The new GameAction.
	 * @param keyCode The key code specified.
	 */
	public void mapToKey(GameAction gameAction, int keyCode) {
		keyActions[keyCode] = gameAction;
	}
	
	/**
	 * Maps a GameAction to a specific mouse action. The mouse codes are defined here in the InputManager
	 * (MOUSE_MOVE_LEFT, MOUSE_BUTTON_1, etc). If the mouse action has a GameAction mapped to it, the new 
	 * GameAction overwrites it.
	 * @param gameAction The new GameAction. 
	 * @param mouseCode The mouse code specified.
	 */
	public void mapToMouse(GameAction gameAction, int mouseCode) {
		mouseActions[mouseCode] = gameAction;
	}
	
	/**
	 * Clears all mapped keys and mouse actions to this GameAction.
	 * @param gameAction The specified GameAction.
	 */
	public void clearMap(GameAction gameAction) {
		for (int k = 0; k < keyActions.length; k++) {
			if (keyActions[k] == gameAction) {
				keyActions[k] = null;
			}
		}
		
		for (int k = 0; k < mouseActions.length; k++) {
			if (mouseActions[k] == gameAction) {
				mouseActions[k] = null;
			}
		}
		
		gameAction.reset();
	}
	
	/**
	 * Gets a List of names of the keys and mouse actions mapped 
	 * to this GameAction. 
	 * @param gameAction The specified GameAction.
	 * @return A List of String.
	 */
	public List<String> getMaps(GameAction gameAction) {
		ArrayList<String> list = new ArrayList<String>();
		
		for (int k = 0; k < keyActions.length; k++) {
			if (keyActions[k] == gameAction) {
				list.add(getKeyName(k));
			}
		}
		
		for (int k = 0; k < mouseActions.length; k++) {
			if (mouseActions[k] == gameAction) {
				list.add(getMouseName(k));
			}
		}
		
		return list;
	}
	
	/**
	 * Resets all GameActions so they appear like they haven't been pressed.
	 */
	public void resetAllGameActions() {
		for (GameAction gameAction : keyActions) {
			if (gameAction != null) gameAction.reset();
		}
		
		for (GameAction gameAction : mouseActions) {
			if (gameAction != null) gameAction.reset();
		}
	}
	
	/**
	 * Returns the name of a key code.
	 * @param keyCode The key code specified.
	 * @return Its name.
	 */
	public static String getKeyName(int keyCode) {
		return KeyEvent.getKeyText(keyCode);
	}
	
	/**
	 * Returns the name of a mouse code.
	 * @param mouseCode The mouse code specified.
	 * @return The name of the mouse code.
	 */
	public static String getMouseName(int mouseCode) {
		switch(mouseCode) {
			case MOUSE_MOVE_LEFT: return "Mouse Left";
			case MOUSE_MOVE_RIGHT: return "Mouse Right";
			case MOUSE_MOVE_UP: return "Mouse Up";
			case MOUSE_MOVE_DOWN: return "Mouse Down";
			case MOUSE_WHEEL_UP: return "Mouse Wheel Up";
			case MOUSE_WHEEL_DOWN: return "Mouse Wheel Down";
			case MOUSE_BUTTON_1: return "Mouse Button 1";
			case MOUSE_BUTTON_2: return "Mouse Button 2";
			case MOUSE_BUTTON_3: return "Mouse Button 3";
			default: return "Unknown mouse code " + mouseCode;
		}
	}
	
	/**
	 * Returns the x position of the mouse.
	 * @return the x position of the mouse.
	 */
	public int getMouseX() {
		return mouseLocation.x;
	}
	
	/**
	 * Returns the y position of the mouse.
	 * @return the y position of the mouse.
	 */
	public int getMouseY() {
		return mouseLocation.y;
	}
	
	/**
	 * Uses the Robot class to try to position the mouse in the center of the screen.
	 * <p>
	 * Note that use of the Robot class may note be available on all platforms.
	 */
	private synchronized void recenterMouse() {
		if (robot != null && comp.isShowing()) {
			centerLocation.x = comp.getWidth() / 2;
			centerLocation.y = comp.getHeight() / 2;
			SwingUtilities.convertPointToScreen(centerLocation, comp);
			isRecentering = true;
			robot.mouseMove(centerLocation.x, centerLocation.y);
		}
	}
	
	/**
	 * Returns the GameAction matching the specified KeyEvent.
	 * @param e The KeyEvent.
	 * @return the GameAction matching the specified KeyEvent. <code>null</code> if there's no such one.
	 */
	private GameAction getKeyAction(KeyEvent e) {
		int keyCode = e.getKeyCode();
		
		if (keyCode < keyActions.length) {
			return keyActions[keyCode];
		} else {
			return null;
		}
	}
	
	/**
	 * Gets the mouse code for the button specified in this MouseEvent.
	 * @param e The MouseEvent.
	 * @return the mouse code for the button specified in this MouseEvent. <code>-1</code> case
	 * no such button exists.
	 */
	public static int getMouseButtonCode(MouseEvent e) {
		switch(e.getButton()) {
			case MouseEvent.BUTTON1: return MOUSE_BUTTON_1;
			case MouseEvent.BUTTON2: return MOUSE_BUTTON_2;
			case MouseEvent.BUTTON3: return MOUSE_BUTTON_3;
			default: return -1;
		}
	}
	
	/**
	 * Returns the GameAction matching to this MouseEvent.
	 * @param e The MouseEvent.
	 * @return the GameAction matching to this MouseEvent. <code>null</code> case there's no matching.
	 */
	private GameAction getMouseButtonAction(MouseEvent e) {
		int mouseCode = getMouseButtonCode(e);
		if (mouseCode != -1) {
			return mouseActions[mouseCode];
		} else {
			return null;
		}
	}
	
	/* (non-Javadoc)
	 * @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent)
	 */
	public void keyPressed(KeyEvent e) {
		GameAction gameAction = getKeyAction(e);
		if (gameAction != null) {
			gameAction.press();
		}
		
		e.consume();
	}
	
	/* (non-Javadoc)
	 * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent)
	 */
	public void keyReleased(KeyEvent e) {
		GameAction gameAction = getKeyAction(e);
		if (gameAction != null) {
			gameAction.release();
		}
		
		e.consume();
	}
	
	/* (non-Javadoc)
	 * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent)
	 */
	public void keyTyped(KeyEvent e) {
		e.consume();
	}
	
	/* (non-Javadoc)
	 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
	 */
	public void mousePressed(MouseEvent e) {
		GameAction gameAction = getMouseButtonAction(e);
		if (gameAction != null) {
			gameAction.press();
		}
	}
	
	/* (non-Javadoc)
	 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
	 */
	public void mouseReleased(MouseEvent e) {
		GameAction gameAction = getMouseButtonAction(e);
		if (gameAction != null) {
			gameAction.release();
		}
	}
	
	/* (non-Javadoc)
	 * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
	 */
	public void mouseClicked(MouseEvent e) {
		// nothing
	}
	
	/* (non-Javadoc)
	 * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
	 */
	public void mouseEntered(MouseEvent e) {
		mouseMoved(e);
	}
	
	/* (non-Javadoc)
	 * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
	 */
	public void mouseExited(MouseEvent e) {
		mouseMoved(e);
	}
	
	/* (non-Javadoc)
	 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
	 */
	public void mouseDragged(MouseEvent e) {
		mouseMoved(e);
	}
	
	/* (non-Javadoc)
	 * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
	 */
	public synchronized void mouseMoved(MouseEvent e) {
		// ignoring event case it is from re-centering the mouse.
		if (isRecentering && centerLocation.x == e.getX() && centerLocation.y == e.getY()) {
			isRecentering = false;
		} else {
			int dx = e.getX() - mouseLocation.x;
			int dy = e.getY() - mouseLocation.y;
			
			mouseHelper(MOUSE_MOVE_LEFT, MOUSE_MOVE_RIGHT, dx);
			mouseHelper(MOUSE_MOVE_UP, MOUSE_MOVE_DOWN, dy);
			
			if (isRelativeMouseMode()) recenterMouse();
		}
		
		mouseLocation.x = e.getX();
		mouseLocation.y = e.getY();
	}
	
	/* (non-Javadoc)
	 * @see java.awt.event.MouseWheelListener#mouseWheelMoved(java.awt.event.MouseWheelEvent)
	 */
	public void mouseWheelMoved(MouseWheelEvent e) {
		mouseHelper(MOUSE_WHEEL_UP, MOUSE_WHEEL_DOWN, e.getWheelRotation());
	}
	
	/**
	 * Helper method to decide and invoked mouse game actions.
	 * @param codeNeg The mouse code case to be invoked case amount is negative.  
	 * @param codePos The mouse code case to be invoked case amount is positive.
	 * @param amount The amount for GameAction.press(int) method.
	 */
	private void mouseHelper(int codeNeg, int codePos, int amount) {
		GameAction gameAction;
		
		if (amount < 0) {
			gameAction = mouseActions[codeNeg];
		} else {
			gameAction = mouseActions[codePos];
		}
		
		if (gameAction != null) {
			gameAction.press(Math.abs(amount));
			gameAction.release();
		}
		
	}
}
